package vehicle

// LICENSE

// Copyright (c) 2019-2022 andig

// This module is NOT covered by the MIT license. All rights reserved.

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"time"

	"github.com/evcc-io/evcc/api"
	"github.com/evcc-io/evcc/util"
	"github.com/evcc-io/evcc/util/oauth"
	"github.com/evcc-io/evcc/util/request"
	"github.com/evcc-io/evcc/util/sponsor"
	"github.com/evcc-io/evcc/vehicle/tronity"
	"golang.org/x/oauth2"
)

// Tronity is an api.Vehicle implementation for the Tronity api
type Tronity struct {
	*embed
	*request.Helper
	log   *util.Logger
	oc    *oauth2.Config
	vid   string
	bulkG func() (tronity.Bulk, error)
}

func init() {
	registry.Add("tronity", NewTronityFromConfig)
}

//go:generate go tool decorate -f decorateTronity -b *Tronity -r api.Vehicle -t "api.ChargeState,Status,func() (api.ChargeStatus, error)" -t "api.VehicleOdometer,Odometer,func() (float64, error)" -t "api.ChargeController,ChargeEnable,func(bool) error"

// NewTronityFromConfig creates a new vehicle
func NewTronityFromConfig(other map[string]interface{}) (api.Vehicle, error) {
	cc := struct {
		embed       `mapstructure:",squash"`
		Credentials ClientCredentials
		Tokens      Tokens
		VIN         string
		Cache       time.Duration
	}{
		Cache: interval,
	}

	if err := util.DecodeOther(other, &cc); err != nil {
		return nil, err
	}

	if err := cc.Credentials.Error(); err != nil {
		return nil, err
	}

	if !sponsor.IsAuthorized() {
		return nil, api.ErrSponsorRequired
	}

	// authenticated http client with logging injected to the tronity client
	log := util.NewLogger("tronity").Redact(cc.Credentials.ID, cc.Credentials.Secret)

	oc, err := tronity.OAuth2Config(cc.Credentials.ID, cc.Credentials.Secret)
	if err != nil {
		return nil, err
	}

	v := &Tronity{
		log:    log,
		embed:  &cc.embed,
		Helper: request.NewHelper(log),
		oc:     oc,
	}

	var ts oauth2.TokenSource
	token, err := cc.Tokens.Token()

	// https://app.platform.tronity.io/docs#tag/Authentication
	if err != nil {
		// use app flow if we don't have tokens
		ts = oauth.RefreshTokenSource(nil, v)
	} else {
		// use provided tokens generated by code flow
		ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
		ts = oc.TokenSource(ctx, token)
	}

	// replace client transport with authenticated transport
	v.Client.Transport = &oauth2.Transport{
		Source: ts,
		Base:   v.Client.Transport,
	}

	vehicle, err := ensureVehicleEx(
		cc.VIN, v.vehicles,
		func(v tronity.Vehicle) (string, error) {
			return v.VIN, nil
		},
	)
	if err != nil {
		return nil, err
	}

	v.vid = vehicle.ID
	v.bulkG = util.Cached(v.bulk, cc.Cache)

	var status func() (api.ChargeStatus, error)
	if slices.Contains(vehicle.Scopes, tronity.ReadCharge) {
		status = v.status
	}

	var odometer func() (float64, error)
	if slices.Contains(vehicle.Scopes, tronity.ReadOdometer) {
		odometer = v.odometer
	}

	var chargeEnable func(bool) error
	if slices.Contains(vehicle.Scopes, tronity.WriteChargeStartStop) {
		chargeEnable = v.chargeEnable
	}

	return decorateTronity(v, status, odometer, chargeEnable), nil
}

// RefreshToken performs token refresh by logging in with app context
func (v *Tronity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
	data := struct {
		ClientID     string `json:"client_id"`
		ClientSecret string `json:"client_secret"`
		GrantType    string `json:"grant_type"`
	}{
		ClientID:     v.oc.ClientID,
		ClientSecret: v.oc.ClientSecret,
		GrantType:    "app",
	}

	req, _ := request.New(http.MethodPost, v.oc.Endpoint.TokenURL, request.MarshalJSON(data), request.JSONEncoding)

	var token oauth2.Token
	err := request.NewHelper(v.log).DoJSON(req, &token)

	return util.TokenWithExpiry(&token), err
}

// vehicles implements the vehicles api
func (v *Tronity) vehicles() ([]tronity.Vehicle, error) {
	uri := fmt.Sprintf("%s/tronity/vehicles", tronity.URI)

	var res tronity.Vehicles
	err := v.GetJSON(uri, &res)

	return res.Data, err
}

// bulk implements the bulk api
func (v *Tronity) bulk() (tronity.Bulk, error) {
	uri := fmt.Sprintf("%s/tronity/vehicles/%s/last_record", tronity.URI, v.vid)

	var res tronity.Bulk
	err := v.GetJSON(uri, &res)

	return res, err
}

// Soc implements the api.Vehicle interface
func (v *Tronity) Soc() (float64, error) {
	res, err := v.bulkG()
	return res.Level, err
}

// status implements the api.ChargeState interface
func (v *Tronity) status() (api.ChargeStatus, error) {
	status := api.StatusA // disconnected
	res, err := v.bulkG()
	if err != nil {
		return status, err
	}

	switch {
	case res.Charging == "Charging":
		status = api.StatusC
	case res.Plugged:
		status = api.StatusB
	}

	return status, nil
}

var _ api.VehicleRange = (*Tronity)(nil)

// Range implements the api.VehicleRange interface
func (v *Tronity) Range() (int64, error) {
	res, err := v.bulkG()
	return int64(res.Range), err
}

// odometer implements the api.VehicleOdometer interface
func (v *Tronity) odometer() (float64, error) {
	res, err := v.bulkG()
	return res.Odometer, err
}

func (v *Tronity) post(uri string) error {
	resp, err := v.Post(uri, "", nil)
	if err == nil {
		err = request.ResponseError(resp)
	}

	// ignore HTTP 405
	if se := new(request.StatusError); errors.As(err, &se) && se.StatusCode() == http.StatusMethodNotAllowed {
		err = nil
	}

	return err
}

// chargeEnable implements the api.ChargeController interface
func (v *Tronity) chargeEnable(enable bool) error {
	action := map[bool]string{true: "start_charging", false: "stop_charging"}
	uri := fmt.Sprintf("%s/tronity/vehicles/%s/%s", tronity.URI, v.vid, action[enable])
	return v.post(uri)
}
